/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.apis.accessibility;

import com.example.android.apis.R;

import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * This sample demonstrates how a View can expose a virtual view sub-tree rooted
 * at it. A virtual sub-tree is composed of imaginary Views that are reported as
 * a part of the view hierarchy for accessibility purposes. This enables custom
 * views that draw complex content to report them selves as a tree of virtual
 * views, thus conveying their logical structure.
 * <p>
 * For example, a View may draw a monthly calendar as a grid of days while each
 * such day may contains some events. From a perspective of the View hierarchy
 * the calendar is composed of a single View but an accessibility service would
 * benefit of traversing the logical structure of the calendar by examining each
 * day and each event on that day.
 * </p>
 */
public class AccessibilityNodeProviderActivity extends Activity {
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.accessibility_node_provider);
	}

	/**
	 * This class presents a View that is composed of three virtual children
	 * each of which is drawn with a different color and represents a region of
	 * the View that has different semantics compared to other such regions.
	 * While the virtual view tree exposed by this class is one level deep for
	 * simplicity, there is no bound on the complexity of that virtual sub-tree.
	 */
	public static class VirtualSubtreeRootView extends View {

		/** Paint object for drawing the virtual sub-tree */
		private final Paint mPaint = new Paint();

		/** Temporary rectangle to minimize object creation. */
		private final Rect mTempRect = new Rect();

		/** Handle to the system accessibility service. */
		private final AccessibilityManager mAccessibilityManager;

		/** The virtual children of this View. */
		private final List<VirtualView> mChildren = new ArrayList<VirtualView>();

		/**
		 * The instance of the node provider for the virtual tree - lazily
		 * instantiated.
		 */
		private AccessibilityNodeProvider mAccessibilityNodeProvider;

		/** The last hovered child used for event dispatching. */
		private VirtualView mLastHoveredChild;

		public VirtualSubtreeRootView(Context context, AttributeSet attrs) {
			super(context, attrs);
			mAccessibilityManager = (AccessibilityManager) context
					.getSystemService(Service.ACCESSIBILITY_SERVICE);
			createVirtualChildren();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public AccessibilityNodeProvider getAccessibilityNodeProvider() {
			// Instantiate the provide only when requested. Since the system
			// will call this method multiple times it is a good practice to
			// cache the provider instance.
			if (mAccessibilityNodeProvider == null) {
				mAccessibilityNodeProvider = new VirtualDescendantsProvider();
			}
			return mAccessibilityNodeProvider;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public boolean dispatchHoverEvent(MotionEvent event) {
			// This implementation assumes that the virtual children
			// cannot overlap and are always visible. Do NOT use this
			// code as a reference of how to implement hover event
			// dispatch. Instead, refer to ViewGroup#dispatchHoverEvent.
			boolean handled = false;
			List<VirtualView> children = mChildren;
			final int childCount = children.size();
			for (int i = 0; i < childCount; i++) {
				VirtualView child = children.get(i);
				Rect childBounds = child.mBounds;
				final int childCoordsX = (int) event.getX() + getScrollX();
				final int childCoordsY = (int) event.getY() + getScrollY();
				if (!childBounds.contains(childCoordsX, childCoordsY)) {
					continue;
				}
				final int action = event.getAction();
				switch (action) {
				case MotionEvent.ACTION_HOVER_ENTER: {
					mLastHoveredChild = child;
					handled |= onHoverVirtualView(child, event);
					event.setAction(action);
				}
					break;
				case MotionEvent.ACTION_HOVER_MOVE: {
					if (child == mLastHoveredChild) {
						handled |= onHoverVirtualView(child, event);
						event.setAction(action);
					} else {
						MotionEvent eventNoHistory = event.getHistorySize() > 0 ? MotionEvent
								.obtainNoHistory(event) : event;
						eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
						onHoverVirtualView(mLastHoveredChild, eventNoHistory);
						eventNoHistory
								.setAction(MotionEvent.ACTION_HOVER_ENTER);
						onHoverVirtualView(child, eventNoHistory);
						mLastHoveredChild = child;
						eventNoHistory.setAction(MotionEvent.ACTION_HOVER_MOVE);
						handled |= onHoverVirtualView(child, eventNoHistory);
						if (eventNoHistory != event) {
							eventNoHistory.recycle();
						} else {
							event.setAction(action);
						}
					}
				}
					break;
				case MotionEvent.ACTION_HOVER_EXIT: {
					mLastHoveredChild = null;
					handled |= onHoverVirtualView(child, event);
					event.setAction(action);
				}
					break;
				}
			}
			if (!handled) {
				handled |= onHoverEvent(event);
			}
			return handled;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		protected void onLayout(boolean changed, int left, int top, int right,
				int bottom) {
			// The virtual children are ordered horizontally next to
			// each other and take the entire space of this View.
			int offsetX = 0;
			List<VirtualView> children = mChildren;
			final int childCount = children.size();
			for (int i = 0; i < childCount; i++) {
				VirtualView child = children.get(i);
				Rect childBounds = child.mBounds;
				childBounds.set(offsetX, 0, offsetX + childBounds.width(),
						childBounds.height());
				offsetX += childBounds.width();
			}
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
			// The virtual children are ordered horizontally next to
			// each other and take the entire space of this View.
			int width = 0;
			int height = 0;
			List<VirtualView> children = mChildren;
			final int childCount = children.size();
			for (int i = 0; i < childCount; i++) {
				VirtualView child = children.get(i);
				width += child.mBounds.width();
				height = Math.max(height, child.mBounds.height());
			}
			setMeasuredDimension(width, height);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		protected void onDraw(Canvas canvas) {
			// Draw the virtual children with the reusable Paint object
			// and with the bounds and color which are child specific.
			Rect drawingRect = mTempRect;
			List<VirtualView> children = mChildren;
			final int childCount = children.size();
			for (int i = 0; i < childCount; i++) {
				VirtualView child = children.get(i);
				drawingRect.set(child.mBounds);
				mPaint.setColor(child.mColor);
				mPaint.setAlpha(child.mAlpha);
				canvas.drawRect(drawingRect, mPaint);
			}
		}

		/**
		 * Creates the virtual children of this View.
		 */
		private void createVirtualChildren() {
			// The virtual portion of the tree is one level deep. Note
			// that implementations can use any way of representing and
			// drawing virtual view.
			VirtualView firstChild = new VirtualView(0,
					new Rect(0, 0, 150, 150), Color.RED, "Virtual view 1");
			mChildren.add(firstChild);
			VirtualView secondChild = new VirtualView(1, new Rect(0, 0, 150,
					150), Color.GREEN, "Virtual view 2");
			mChildren.add(secondChild);
			VirtualView thirdChild = new VirtualView(2,
					new Rect(0, 0, 150, 150), Color.BLUE, "Virtual view 3");
			mChildren.add(thirdChild);
		}

		/**
		 * Set the selected state of a virtual view.
		 * 
		 * @param virtualView
		 *            The virtual view whose selected state to set.
		 * @param selected
		 *            Whether the virtual view is selected.
		 */
		private void setVirtualViewSelected(VirtualView virtualView,
				boolean selected) {
			virtualView.mAlpha = selected ? VirtualView.ALPHA_SELECTED
					: VirtualView.ALPHA_NOT_SELECTED;
		}

		/**
		 * Handle a hover over a virtual view.
		 * 
		 * @param virtualView
		 *            The virtual view over which is hovered.
		 * @param event
		 *            The event to dispatch.
		 * @return Whether the event was handled.
		 */
		private boolean onHoverVirtualView(VirtualView virtualView,
				MotionEvent event) {
			// The implementation of hover event dispatch can be implemented
			// in any way that is found suitable. However, each virtual View
			// should fire a corresponding accessibility event whose source
			// is that virtual view. Accessibility services get the event source
			// as the entry point of the APIs for querying the window content.
			final int action = event.getAction();
			switch (action) {
			case MotionEvent.ACTION_HOVER_ENTER: {
				sendAccessibilityEventForVirtualView(virtualView,
						AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
			}
				break;
			case MotionEvent.ACTION_HOVER_EXIT: {
				sendAccessibilityEventForVirtualView(virtualView,
						AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
			}
				break;
			}
			return true;
		}

		/**
		 * Sends a properly initialized accessibility event for a virtual view..
		 * 
		 * @param virtualView
		 *            The virtual view.
		 * @param eventType
		 *            The type of the event to send.
		 */
		private void sendAccessibilityEventForVirtualView(
				VirtualView virtualView, int eventType) {
			// If touch exploration, i.e. the user gets feedback while touching
			// the screen, is enabled we fire accessibility events.
			if (mAccessibilityManager.isTouchExplorationEnabled()) {
				AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
				event.setPackageName(getContext().getPackageName());
				event.setClassName(virtualView.getClass().getName());
				event.setSource(VirtualSubtreeRootView.this, virtualView.mId);
				event.getText().add(virtualView.mText);
				getParent().requestSendAccessibilityEvent(
						VirtualSubtreeRootView.this, event);
			}
		}

		/**
		 * Finds a virtual view given its id.
		 * 
		 * @param id
		 *            The virtual view id.
		 * @return The found virtual view.
		 */
		private VirtualView findVirtualViewById(int id) {
			List<VirtualView> children = mChildren;
			final int childCount = children.size();
			for (int i = 0; i < childCount; i++) {
				VirtualView child = children.get(i);
				if (child.mId == id) {
					return child;
				}
			}
			return null;
		}

		/**
		 * Represents a virtual View.
		 */
		private class VirtualView {
			public static final int ALPHA_SELECTED = 255;
			public static final int ALPHA_NOT_SELECTED = 127;

			public final int mId;
			public final int mColor;
			public final Rect mBounds;
			public final String mText;
			public int mAlpha;

			public VirtualView(int id, Rect bounds, int color, String text) {
				mId = id;
				mColor = color;
				mBounds = bounds;
				mText = text;
				mAlpha = ALPHA_NOT_SELECTED;
			}
		}

		/**
		 * This is the provider that exposes the virtual View tree to
		 * accessibility services. From the perspective of an accessibility
		 * service the {@link AccessibilityNodeInfo}s it receives while
		 * exploring the sub-tree rooted at this View will be the same as the
		 * ones it received while exploring a View containing a sub-tree
		 * composed of real Views.
		 */
		private class VirtualDescendantsProvider extends
				AccessibilityNodeProvider {

			/**
			 * {@inheritDoc}
			 */
			@Override
			public AccessibilityNodeInfo createAccessibilityNodeInfo(
					int virtualViewId) {
				AccessibilityNodeInfo info = null;
				if (virtualViewId == View.NO_ID) {
					// We are requested to create an AccessibilityNodeInfo
					// describing
					// this View, i.e. the root of the virtual sub-tree. Note
					// that the
					// host View has an AccessibilityNodeProvider which means
					// that this
					// provider is responsible for creating the node info for
					// that root.
					info = AccessibilityNodeInfo
							.obtain(VirtualSubtreeRootView.this);
					onInitializeAccessibilityNodeInfo(info);
					// Add the virtual children of the root View.
					List<VirtualView> children = mChildren;
					final int childCount = children.size();
					for (int i = 0; i < childCount; i++) {
						VirtualView child = children.get(i);
						info.addChild(VirtualSubtreeRootView.this, child.mId);
					}
				} else {
					// Find the view that corresponds to the given id.
					VirtualView virtualView = findVirtualViewById(virtualViewId);
					if (virtualView == null) {
						return null;
					}
					// Obtain and initialize an AccessibilityNodeInfo with
					// information about the virtual view.
					info = AccessibilityNodeInfo.obtain();
					info.addAction(AccessibilityNodeInfo.ACTION_SELECT);
					info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
					info.setPackageName(getContext().getPackageName());
					info.setClassName(virtualView.getClass().getName());
					info.setSource(VirtualSubtreeRootView.this, virtualViewId);
					info.setBoundsInParent(virtualView.mBounds);
					info.setParent(VirtualSubtreeRootView.this);
					info.setText(virtualView.mText);
				}
				return info;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(
					String searched, int virtualViewId) {
				if (TextUtils.isEmpty(searched)) {
					return Collections.emptyList();
				}
				String searchedLowerCase = searched.toLowerCase();
				List<AccessibilityNodeInfo> result = null;
				if (virtualViewId == View.NO_ID) {
					// If the search is from the root, i.e. this View, go over
					// the virtual
					// children and look for ones that contain the searched
					// string since
					// this View does not contain text itself.
					List<VirtualView> children = mChildren;
					final int childCount = children.size();
					for (int i = 0; i < childCount; i++) {
						VirtualView child = children.get(i);
						String textToLowerCase = child.mText.toLowerCase();
						if (textToLowerCase.contains(searchedLowerCase)) {
							if (result == null) {
								result = new ArrayList<AccessibilityNodeInfo>();
							}
							result.add(createAccessibilityNodeInfo(child.mId));
						}
					}
				} else {
					// If the search is from a virtual view, find the view.
					// Since the tree
					// is one level deep we add a node info for the child to the
					// result if
					// the child contains the searched text.
					VirtualView virtualView = findVirtualViewById(virtualViewId);
					if (virtualView != null) {
						String textToLowerCase = virtualView.mText
								.toLowerCase();
						if (textToLowerCase.contains(searchedLowerCase)) {
							result = new ArrayList<AccessibilityNodeInfo>();
							result.add(createAccessibilityNodeInfo(virtualViewId));
						}
					}
				}
				if (result == null) {
					return Collections.emptyList();
				}
				return result;
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public boolean performAction(int virtualViewId, int action,
					Bundle arguments) {
				if (virtualViewId == View.NO_ID) {
					// Perform the action on the host View.
					switch (action) {
					case AccessibilityNodeInfo.ACTION_SELECT:
						if (!isSelected()) {
							setSelected(true);
							return isSelected();
						}
						break;
					case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION:
						if (isSelected()) {
							setSelected(false);
							return !isSelected();
						}
						break;
					}
				} else {
					// Find the view that corresponds to the given id.
					VirtualView child = findVirtualViewById(virtualViewId);
					if (child == null) {
						return false;
					}
					// Perform the action on a virtual view.
					switch (action) {
					case AccessibilityNodeInfo.ACTION_SELECT:
						setVirtualViewSelected(child, true);
						invalidate();
						return true;
					case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION:
						setVirtualViewSelected(child, false);
						invalidate();
						return true;
					}
				}
				return false;
			}
		}
	}
}
